﻿using System.Collections.Generic;
using System.Linq;
using System.Net;
using JetBlack.AuthFeedBus.Messages;
using JetBlack.Diagnostics;

namespace JetBlack.AuthFeedBus.Distributor
{
    internal class SubscriptionManager
    {
        private class ClientEntitlements
        {
            public readonly bool IsFieldLevel;
            public readonly IDictionary<string, object> FieldEntitlements;

            public ClientEntitlements(bool isFieldLevel, IDictionary<string, object> fieldEntitlements)
            {
                IsFieldLevel = isFieldLevel;
                FieldEntitlements = fieldEntitlements;
            }
        }

        private class InteractorCache : Dictionary<Interactor, ClientEntitlements> { }

        private class SubscriptionCache : Dictionary<string, InteractorCache>
        {
            public readonly bool RequiresEntitlement;

            public SubscriptionCache(string feed)
            {
                Config.FeedConfiguration feedConfiguration;
                if (!AppEnvironment.Configuration.FeedConfigurations.TryGetValue(feed, out feedConfiguration))
                    RequiresEntitlement = false;
                else
                    RequiresEntitlement = feedConfiguration.RequiresEntitlement;
            }
        }

        private class FeedCache : Dictionary<string, SubscriptionCache> { }

        internal delegate void OnSubscriptionHandler(ForwardedSubscriptionRequest message);
        internal event OnSubscriptionHandler OnSubscription;

        internal delegate void OnClientMessageHandler(Interactor publisher, Interactor subscriber, ClientMessage message);
        internal event OnClientMessageHandler OnClientMessage;

        internal delegate void OnSubscriberMessageHandler(Interactor publisher, Interactor subscriber, SubscriberMessage message);
        internal event OnSubscriberMessageHandler OnSubscriberMessage;

        internal delegate void OnStaleTopicHandler(Interactor publisher, Interactor subscriber, string feed, string topic);
        internal event OnStaleTopicHandler OnStaleTopic;

        private readonly FeedCache feedCache = new FeedCache();

        internal void HandleClientMessage(ClientMessage message, Interactor publisher)
        {
            if (OnClientMessage == null)
                return;

            SubscriptionCache subscriptionCache;
            if (!feedCache.TryGetValue(message.Feed, out subscriptionCache))
                return;

            InteractorCache interactorCache;
            if (!subscriptionCache.TryGetValue(message.Topic, out interactorCache))
                return;

            foreach (var subscriber in interactorCache.Where(s => s.Key.Id == message.ClientId))
            {
                if (subscriptionCache.RequiresEntitlement)
                    PublishFilteredClientMessage(publisher, subscriber.Key, subscriber.Value, message);
                else
                    PublishClientMessage(publisher, subscriber.Key, message);
            }
        }

        internal void HandleSubscriberMessage(SubscriberMessage message, Interactor publisher)
        {
            if (OnSubscriberMessage == null)
                return;

            SubscriptionCache subscriptionCache;
            if (!feedCache.TryGetValue(message.Feed, out subscriptionCache))
                return;

            InteractorCache interactorCache;
            if (!subscriptionCache.TryGetValue(message.Topic, out interactorCache))
                return;

            foreach (var subscriber in interactorCache)
            {
                if (subscriptionCache.RequiresEntitlement)
                    PublishFilteredSubscriberMessage(publisher, subscriber.Key, subscriber.Value, message);
                else
                    PublishSubscriberMessage(publisher, subscriber.Key, message);
            }
        }

        internal void HandleSubscription(SubscriptionRequest message, Interactor subscriber)
        {
            AppEnvironment.TraceSource.Verbose("Received subscription from {0} on \"{1}\"", subscriber, message);

            if (message.IsAdd)
                AddSubscription(message.Feed, message.Topic, subscriber);
            else
                RemoveSubscription(message.Feed, message.Topic, subscriber);
        }

        private void AddSubscription(string feed, string topic, Interactor subscriber)
        {
            SubscriptionCache subscriptionCache;
            if (!feedCache.TryGetValue(feed, out subscriptionCache))
                feedCache.Add(feed, subscriptionCache = new SubscriptionCache(feed));

            // Prepare the feed/topic/field/value area
            InteractorCache interactorCache;
            if (!subscriptionCache.TryGetValue(topic, out interactorCache))
                subscriptionCache.Add(topic, interactorCache = new InteractorCache());

            // Find the list of interactors that have subscribed to this topic.
            if (!interactorCache.ContainsKey(subscriber))
                interactorCache.Add(subscriber, null);

            // Inform those waiting for this request
            NotifyListeners(subscriber.Id, subscriber.RemoteEndPoint.Address, subscriber.User.Name, feed, topic, true);
        }

        private void RemoveSubscription(string feed, string topic, Interactor subscriber)
        {
            SubscriptionCache subscriptionCache;
            if (!feedCache.TryGetValue(feed, out subscriptionCache))
                return;

            // Prepare the feed/topic/field/value area
            InteractorCache interactorCache;
            if (!subscriptionCache.TryGetValue(topic, out interactorCache))
                return;

            // Find the list of interactors that have subscribed to this topic.
            if (interactorCache.ContainsKey(subscriber))
                interactorCache.Remove(subscriber);

            if (interactorCache.Count == 0)
            {
                subscriptionCache.Remove(topic);

                if (feedCache.Count == 0)
                    feedCache.Remove(feed);
            }

            // Inform those waiting for this request
            NotifyListeners(subscriber.Id, subscriber.RemoteEndPoint.Address, subscriber.User.Name, feed, topic, false);
        }

        internal void Remove(Interactor subscriber)
        {
            AppEnvironment.TraceSource.Verbose("Removing subscriptions for {0}", subscriber);

            var emptyFeeds = new List<string>();
            foreach (var feedItem in feedCache)
            {
                var emptySubscriptions = new List<string>();

                foreach (var subscriptionItem in feedItem.Value)
                {
                    if (subscriptionItem.Value.ContainsKey(subscriber))
                    {
                        subscriptionItem.Value.Remove(subscriber);
                        if (subscriptionItem.Value.Count == 0)
                            emptySubscriptions.Add(subscriptionItem.Key);
                        NotifyListeners(subscriber.Id, subscriber.RemoteEndPoint.Address, subscriber.User.Name, feedItem.Key, subscriptionItem.Key, false);
                    }
                }

                foreach (string topic in emptySubscriptions)
                    feedItem.Value.Remove(topic);

                if (feedItem.Value.Count == 0)
                    emptyFeeds.Add(feedItem.Key);
            }

            foreach (string feed in emptyFeeds)
                feedCache.Remove(feed);
        }

        private void NotifyListeners(int clientId, IPAddress ipAddress, string user, string feed, string topic, bool isAdd)
        {
            if (OnSubscription != null)
                OnSubscription(new ForwardedSubscriptionRequest(clientId, ipAddress, user, feed, topic, isAdd));
        }

        internal void HandleNotificationResponse(Interactor notifiable, IEnumerable<string> feeds, bool isPublisher, bool isAdd)
        {
            if (isAdd)
                HandleActivePublisher(notifiable, feeds);
            else if (isPublisher)
                // Only notifiers which provide publishing services should generate stale events.
                HandleStalePublisher(notifiable, feeds);
        }

        private void HandleActivePublisher(Interactor notifiable, IEnumerable<string> feeds)
        {
            foreach (string feed in feeds)
                if (feedCache.ContainsKey(feed))
                    foreach (var subscriptionItem in feedCache[feed])
                        foreach (var subscriber in subscriptionItem.Value.Keys)
                            notifiable.SendMessage(new ForwardedSubscriptionRequest(subscriber.Id, subscriber.RemoteEndPoint.Address, subscriber.User.Name, feed, subscriptionItem.Key, true));
        }

        private void HandleStalePublisher(Interactor publisher, IEnumerable<string> feeds)
        {
            foreach (string feed in feeds)
                if (feedCache.ContainsKey(feed))
                    foreach (var subscriptionItem in feedCache[feed])
                        foreach (var subscriber in subscriptionItem.Value.Keys)
                            OnStaleTopic(publisher, subscriber, feed, subscriptionItem.Key);
        }

        internal void HandleEntitlement(Entitlements message, Interactor entitler)
        {
            AppEnvironment.TraceSource.Verbose("Entitlements received from {0} for {1}", entitler, message);

            SubscriptionCache subscriptionCache;
            if (!feedCache.TryGetValue(message.Feed, out subscriptionCache))
                return;

            InteractorCache interactorCache;
            if (!subscriptionCache.TryGetValue(message.Topic, out interactorCache))
                return;

            var subscriber = interactorCache.Keys.FirstOrDefault(i => i.Id == message.ClientId);
            if (subscriber == null)
                return;

            if (message.IsFieldLevel && (message.FieldEntitlements == null || message.FieldEntitlements.Count == 0))
            {
                // Nothing can be sent, so we might as well remove the subscription.
                interactorCache.Remove(subscriber);

                if (interactorCache.Count == 0)
                {
                    subscriptionCache.Remove(message.Topic);

                    if (subscriptionCache.Count == 0)
                        feedCache.Remove(message.Feed);
                }
            }
            else
                interactorCache[subscriber] = new ClientEntitlements(message.IsFieldLevel, message.FieldEntitlements);
        }

        private void PublishSubscriberMessage(Interactor publisher, Interactor subscriber, SubscriberMessage message)
        {
            OnSubscriberMessage(publisher, subscriber, message);
        }

        private void PublishFilteredSubscriberMessage(Interactor publisher, Interactor subscriber, ClientEntitlements entitlementCache, SubscriberMessage message)
        {
            if (entitlementCache == null)
                // No entitlements have been received yet.
                return;
            else if (!entitlementCache.IsFieldLevel)
                // The subscriber has been entitled for all fields, so simply forward on the message.
                OnSubscriberMessage(publisher, subscriber, message);
            else
                // We have fields level entitlements, so filter out the fields the client is not entitled to receive.
                OnSubscriberMessage(publisher, subscriber, new SubscriberMessage(message.Feed, message.Topic, true, FilterData(message.Data, entitlementCache.FieldEntitlements)));
        }

        private void PublishClientMessage(Interactor publisher, Interactor subscriber, ClientMessage message)
        {
            OnClientMessage(publisher, subscriber, message);
        }

        private void PublishFilteredClientMessage(Interactor publisher, Interactor subscriber, ClientEntitlements clientEntitlements, ClientMessage message)
        {
            if (clientEntitlements == null)
                // No entitlements have been received yet.
                return;
            else if (!clientEntitlements.IsFieldLevel)
                // The subscriber has been entitled for all fields, so simply forward on the message.
                OnClientMessage(publisher, subscriber, message);
            else
                // We have fields level entitlements, so filter out the fields the client is not entitled to receive.
                OnClientMessage(publisher, subscriber, new ClientMessage(message.ClientId, message.Feed, message.Topic, true, FilterData(message.Data, clientEntitlements.FieldEntitlements)));
        }

        private IDictionary<string, object> FilterData(IDictionary<string, object> data, IDictionary<string, object> fieldEntitlements)
        {
            var filteredData = new Dictionary<string, object>();

            if (data != null && fieldEntitlements != null)
            {
                foreach (var item in data)
                {
                    object entitlement;
                    if (fieldEntitlements.TryGetValue(item.Key, out entitlement))
                    {
                        var filteredItem = FilterItem(item, entitlement);
                        if (filteredItem != null)
                            filteredData.Add(item.Key, item.Value);
                    }
                }
            }

            return filteredData;
        }

        private object FilterItem(object item, object entitlement)
        {
            if (item is IDictionary<string, object>)
            {
                if (entitlement is IDictionary<string, object>)
                    return FilterData((IDictionary<string, object>)item, (IDictionary<string, object>)entitlement);
                else
                    return null;
            }
            else if (item is object[])
            {
                var list = new List<object>();
                foreach (var member in (object[])item)
                {
                    var value = FilterItem(member, entitlement);
                    if (value != null)
                        list.Add(value);
                }
                if (list.Count > 0)
                    return list.ToArray();
                else
                    return null;
            }
            else if (entitlement != null)
                return item;
            else
                return null;
        }
    }
}
